type ClassExtInfoJSON = {
  type: string
  refer: string
}

type CodeInfoJSON = {
  type: string
  name: string
  ext?: ClassExtInfoJSON[]
  isFinal?: boolean
  onlyDeclare?: boolean
  children?: CodeInfoJSON[]
}

interface ContainerRefer {
  isEmpty(): boolean
}

class NamespaceRefer implements ContainerRefer {
  name: string
  private allClass: Map<string, ClassRefer>
  constructor(name: string) {
    this.name = name
    this.allClass = new Map()
  }
  addClass(name: string, refer: ClassRefer) {
    this.allClass.set(name, refer)
  }
  isEmpty() {
    return this.allClass.size === 0
  }
}

class ClassRefer {
  name: string
  defineFile: string
  private extendsFrom: Map<string, ClassRefer>
  private extendsTo: Map<string, ClassRefer>
  constructor(name: string) {
    this.name = name
    this.defineFile = ''
    this.extendsFrom = new Map()
    this.extendsTo = new Map()
  }
  addExtendsFrom(name: string, refer: ClassRefer) {
    this.extendsFrom.set(name, refer)
  }
  addExtendsTo(name: string, refer: ClassRefer) {
    this.extendsTo.set(name, refer)
  }
  setDefineFile(defineFile: string) {
    this.defineFile = defineFile
  }
  getExtendsFrom(): string[] {
    return Array.from(this.extendsFrom.keys())
  }
  getExtendsTo(): string[] {
    return Array.from(this.extendsTo.keys())
  }
}

export class CodeInfo {
  private allNamespace: Map<string, NamespaceRefer>
  private allClass: Map<string, ClassRefer>

  static fromStaticInfoFile(file: File): Promise<CodeInfo | null> {
    const reader = new FileReader()
    reader.readAsText(file, 'utf-8')
    return new Promise((resolve) => {
      reader.onload = (ev) => {
        const info = JSON.parse(ev.target?.result as string)
        const codeInfo = new CodeInfo()
        codeInfo.recordCodeInfo(info, '', '')
        codeInfo.resetAnonymousNamespaceId()
        codeInfo.updateClassRefer(info, '')
        codeInfo.clearEmptyNamespace()
        resolve(codeInfo)
      }
    })
  }

  constructor() {
    this.allNamespace = new Map()
    this.allClass = new Map()
  }

  recordNamespace(name: string): NamespaceRefer {
    if (this.allNamespace.has(name)) {
      return this.allNamespace.get(name)!
    }
    const refer = new NamespaceRefer(name)
    this.allNamespace.set(name, refer)
    return refer
  }

  recordClass(namespace: string, name: string): ClassRefer {
    const namespaceRefer = this.recordNamespace(namespace)
    const nname = `${namespace}::${name}`

    if (this.allClass.has(nname)) {
      return this.allClass.get(nname)!
    }

    const refer = new ClassRefer(name)
    this.allClass.set(nname, refer)
    namespaceRefer.addClass(nname, refer)
    return refer
  }
  private m_anonymousNamespaceId = 0
  private resetAnonymousNamespaceId() {
    this.m_anonymousNamespaceId = 0
  }
  private resolveNamespaceRefer(namespace: string, name: string) {
    if (name) {
      return namespace + '::' + name
    }
    return `${namespace}::anonymous(${this.m_anonymousNamespaceId++})`
  }

  private findClass(namespace: string, name: string) {
    if (name.startsWith('::')) {
      return name
    }

    // 优先查找当前的命名空间的是否存在, 然后逐层的向上查找
    const currentNamespaceArr = namespace.split('::')
    while (currentNamespaceArr.length > 0) {
      const tryName = [...currentNamespaceArr, name].join('::')
      if (this.allClass.has(tryName)) {
        return tryName
      }
      currentNamespaceArr.pop()
    }

    // 顶层命名空间
    this.allClass.set('::' + name, new ClassRefer(name))
    return '::' + name
  }
  setExtends(namespace: string, subClass: string, superClass: string) {
    if (subClass.endsWith('NumberInput')) {
      console.log()
    }
    const subName = this.findClass(namespace, subClass)
    const superName = this.findClass(namespace, superClass)
    const subRefer = this.allClass.get(subName)
    const superRefer = this.allClass.get(superName)
    if (subRefer && superRefer) {
      subRefer.addExtendsFrom(superName, superRefer)
      superRefer.addExtendsTo(subName, subRefer)
    }
  }
  setClassDefineFile(className: string, defineFile: string) {
    this.allClass.get(className)?.setDefineFile(defineFile)
  }
  recordCodeInfo(infoJSON: CodeInfoJSON[], namespace: string, filePath: string) {
    for (const item of infoJSON) {
      if (item.type === 'namespace') {
        const nname = this.resolveNamespaceRefer(namespace, item.name)
        this.recordNamespace(nname)
        if (item.children) {
          this.recordCodeInfo(item.children, nname, filePath)
        }
      } else if (item.type === 'class') {
        this.recordClass(namespace, item.name)
        if (!item.onlyDeclare) {
          this.setClassDefineFile(item.name, filePath)
        }
      } else if (item.type === 'file') {
        if (item.children) {
          this.recordCodeInfo(item.children!, '', item.name)
        }
      }
    }
  }
  private clearEmptyNamespace() {
    const keepNamespace = new Map<string, NamespaceRefer>()
    this.allNamespace.forEach((refer, name) => {
      if (!refer.isEmpty()) {
        keepNamespace.set(name, refer)
      }
    })
    this.allNamespace = keepNamespace
  }
  updateClassRefer(infoJSON: CodeInfoJSON[], namespace: string) {
    for (const item of infoJSON) {
      if (item.type === 'namespace') {
        if (item.children) {
          this.updateClassRefer(item.children!, this.resolveNamespaceRefer(namespace, item.name))
        }
      } else if (item.type === 'class') {
        if (item.ext) {
          for (const ext of item.ext) {
            this.setExtends(namespace, item.name, ext.refer)
          }
        }
      } else if (item.type === 'file') {
        if (item.children) {
          this.updateClassRefer(item.children!, namespace)
        }
      }
    }
  }
  searchClassName(keyword: string): string[] {
    const result: string[] = []
    this.allClass.forEach((_, name) => {
      if (name.toLowerCase().includes(keyword.toLowerCase())) {
        result.push(name)
      }
    })
    return result
  }
  getExtendsFromClass(className: string): string[] {
    const result: string[] = []
    const classRefer = this.allClass.get(className)
    if (classRefer) {
      result.push(...classRefer.getExtendsFrom())
    }
    return result
  }
  getExtendsToClass(className: string): string[] {
    const result: string[] = []
    const classRefer = this.allClass.get(className)
    if (classRefer) {
      result.push(...classRefer.getExtendsTo())
    }
    return result
  }
  toJSON() {
    return {
      allNamespace: this.allNamespace,
      allClass: this.allClass
    }
  }
}
